#!/bin/bash
#
#  Copyright (c) 2024, The OpenThread Authors.
#  All rights reserved.
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions are met:
#  1. Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#  2. Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#  3. Neither the name of the copyright holder nor the
#     names of its contributors may be used to endorse or promote products
#     derived from this software without specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
#  POSSIBILITY OF SUCH DAMAGE.
#
# Test basic functionality of otbr-agent under NCP mode.
#
# Usage:
#   ./ncp_mode
set -euxo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
readonly SCRIPT_DIR
EXPECT_SCRIPT_DIR="${SCRIPT_DIR}/expect"
readonly EXPECT_SCRIPT_DIR

#---------------------------------------
# Configurations
#---------------------------------------
OT_CLI="${OT_CLI:-ot-cli-ftd}"
readonly OT_CLI

OT_NCP="${OT_NCP:-ot-ncp-ftd}"
readonly OT_NCP

OTBR_DOCKER_IMAGE="${OTBR_DOCKER_IMAGE:-otbr-ncp}"
readonly OTBR_DOCKER_IMAGE

OTBR_MDNS="${OTBR_MDNS:-avahi}"
readonly OTBR_MDNS

ABS_TOP_BUILDDIR="$(cd "${top_builddir:-"${SCRIPT_DIR}"/../../}" && pwd)"
readonly ABS_TOP_BUILDDIR

ABS_TOP_SRCDIR="$(cd "${top_srcdir:-"${SCRIPT_DIR}"/../../}" && pwd)"
readonly ABS_TOP_SRCDIR

ABS_TOP_OT_SRCDIR="${ABS_TOP_SRCDIR}/third_party/openthread/repo"
readonly ABS_TOP_OT_SRCDIR

ABS_TOP_OT_BUILDDIR="${ABS_TOP_BUILDDIR}/../simulation"
readonly ABS_TOP_BUILDDIR

OT_CTL="${ABS_TOP_BUILDDIR}/third_party/openthread/repo/src/posix/ot-ctl"
readonly OT_CTL

OTBR_COLOR_PASS='\033[0;32m'
readonly OTBR_COLOR_PASS

OTBR_COLOR_FAIL='\033[0;31m'
readonly OTBR_COLOR_FAIL

OTBR_COLOR_NONE='\033[0m'
readonly OTBR_COLOR_NONE

readonly OTBR_VERBOSE="${OTBR_VERBOSE:-0}"

#----------------------------------------
# Helper functions
#----------------------------------------
die()
{
    exit_message="$*"
    echo " *** ERROR: $*"
    exit 1
}

exists_or_die()
{
    [[ -f $1 ]] || die "Missing file: $1"
}

executable_or_die()
{
    [[ -x $1 ]] || die "Missing executable: $1"
}

write_syslog()
{
    logger -s -p syslog.alert "OTBR_TEST: $*"
}

#----------------------------------------
# Test constants
#----------------------------------------
TEST_BASE=/tmp/test-otbr
readonly TEST_BASE

OTBR_AGENT=otbr-agent
readonly OTBR_AGENT

STAGE_DIR="${TEST_BASE}/stage"
readonly STAGE_DIR

BUILD_DIR="${TEST_BASE}/build"
readonly BUILD_DIR

OTBR_DBUS_CONF="${ABS_TOP_BUILDDIR}/src/agent/otbr-agent.conf"
readonly OTBR_DBUS_CONF

OTBR_AGENT_PATH="${ABS_TOP_BUILDDIR}/src/agent/${OTBR_AGENT}"
readonly OTBR_AGENT_PATH

# External commissioner
OT_COMMISSIONER_PATH=${ABS_TOP_OT_BUILDDIR}/ot-commissioner/build/src/app/cli/commissioner-cli
readonly OT_COMMISSIONER_PATH

# The node ids
LEADER_NODE_ID=1
readonly LEADER_NODE_ID

# The TUN device for OpenThread border router.
TUN_NAME=wpan0
readonly TUN_NAME

#----------------------------------------
# Test steps
#----------------------------------------
do_build_ot_simulation()
{
    sudo rm -rf "${ABS_TOP_OT_BUILDDIR}/ncp"
    sudo rm -rf "${ABS_TOP_OT_BUILDDIR}/cli"
    OT_CMAKE_BUILD_DIR=${ABS_TOP_OT_BUILDDIR}/ncp "${ABS_TOP_OT_SRCDIR}"/script/cmake-build simulation \
        -DOT_MTD=OFF -DOT_RCP=OFF -DOT_APP_CLI=OFF -DOT_APP_RCP=OFF \
        -DOT_BORDER_ROUTING=ON -DOT_NCP_INFRA_IF=ON -DOT_SIMULATION_INFRA_IF=OFF \
        -DOT_SRP_SERVER=ON -DOT_SRP_ADV_PROXY=ON -DOT_PLATFORM_DNSSD=ON -DOT_SIMULATION_DNSSD=OFF -DOT_NCP_DNSSD=ON \
        -DOT_BORDER_AGENT=ON -DOT_BORDER_AGENT_MESHCOP_SERVICE=OFF \
        -DOT_NCP_CLI_STREAM=ON -DOT_BACKBONE_ROUTER=ON -DOT_BACKBONE_ROUTER_MULTICAST_ROUTING=ON \
        -DBUILD_TESTING=OFF
    OT_CMAKE_BUILD_DIR=${ABS_TOP_OT_BUILDDIR}/cli "${ABS_TOP_OT_SRCDIR}"/script/cmake-build simulation \
        -DOT_MTD=OFF -DOT_RCP=OFF -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
        -DOT_BORDER_ROUTING=OFF -DOT_MLR=ON \
        -DBUILD_TESTING=OFF
}

do_build_ot_commissioner()
{
    if [[ -x ${OT_COMMISSIONER_PATH} ]]; then
        return 0
    fi

    (mkdir -p "${ABS_TOP_OT_BUILDDIR}/ot-commissioner" \
        && cd "${ABS_TOP_OT_BUILDDIR}/ot-commissioner" \
        && (git --git-dir=.git rev-parse --is-inside-work-tree || git --git-dir=.git init .) \
        && git fetch --depth 1 https://github.com/openthread/ot-commissioner.git main \
        && git checkout FETCH_HEAD \
        && ./script/bootstrap.sh \
        && mkdir build && cd build \
        && cmake -GNinja -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DCMAKE_BUILD_TYPE=Release .. \
        && ninja)
}

do_build_otbr_docker()
{
    otbr_docker_options=(
        "-DOT_THREAD_VERSION=1.4"
        "-DOTBR_DBUS=ON"
        "-DOTBR_FEATURE_FLAGS=ON"
        "-DOTBR_TELEMETRY_DATA_API=ON"
        "-DOTBR_LINK_METRICS_TELEMETRY=ON"
        "-DOTBR_BORDER_AGENT=ON"
    )
    sudo docker build -t "${OTBR_DOCKER_IMAGE}" \
        -f ./etc/docker/test/Dockerfile . \
        --build-arg NAT64=0 \
        --build-arg WEB_GUI=0 \
        --build-arg REST_API=0 \
        --build-arg FIREWALL=0 \
        --build-arg OTBR_OPTIONS="${otbr_docker_options[*]}"
}

setup_infraif()
{
    if ! ip link show backbone1 >/dev/null 2>&1; then
        echo "Creating backbone1 with Docker..."
        docker network create --driver bridge --ipv6 --subnet 9101::/64 -o "com.docker.network.bridge.name"="backbone1" backbone1
    else
        echo "backbone1 already exists."
    fi
    sudo sysctl -w net.ipv6.conf.backbone1.accept_ra=2
    sudo sysctl -w net.ipv6.conf.backbone1.accept_ra_rt_info_max_plen=64
    # Delete the 9101::1/ address to ensure ping through backbone1 use an on-link address.
    sudo ip addr del 9101::1/64 dev backbone1
}

test_setup()
{
    executable_or_die "${OTBR_AGENT_PATH}"
    executable_or_die "${OT_CTL}"

    # Remove flashes
    sudo rm -vrf "${TEST_BASE}/tmp"
    # OPENTHREAD_POSIX_DAEMON_SOCKET_LOCK
    sudo rm -vf "/tmp/openthread.lock"

    ot_cli=$(find "${ABS_TOP_OT_BUILDDIR}" -name "${OT_CLI}")
    ot_ncp=$(find "${ABS_TOP_OT_BUILDDIR}" -name "${OT_NCP}")
    executable_or_die "${ot_cli}"
    executable_or_die "${ot_ncp}"

    export EXP_OTBR_AGENT_PATH="${OTBR_AGENT_PATH}"
    export EXP_OT_CTL_PATH="${OT_CTL}"
    export EXP_OT_CLI_PATH="${ot_cli}"
    export EXP_OT_NCP_PATH="${ot_ncp}"

    # We will be creating a lot of log information
    # Rotate logs so we have a clean and empty set of logs uncluttered with other stuff
    if [[ -f /etc/logrotate.conf ]]; then
        sudo logrotate -f /etc/logrotate.conf || true
    fi

    # Preparation for otbr-agent
    exists_or_die "${OTBR_DBUS_CONF}"
    sudo cp "${OTBR_DBUS_CONF}" /etc/dbus-1/system.d
    sudo chmod +r /etc/dbus-1/system.d/otbr-agent.conf
    sudo systemctl reload dbus
    if [[ ${OTBR_MDNS} == "mDNSResponder" ]]; then
        sudo systemctl restart mdns
    fi

    write_syslog "AGENT: kill old"
    sudo killall "${OTBR_AGENT}" || true

    setup_infraif

    # From now on - all exits are TRAPPED
    # When they occur, we call the function: output_logs'.
    trap test_teardown EXIT
}

test_teardown()
{
    # Capture the exit code so we can return it below
    EXIT_CODE=$?
    readonly EXIT_CODE
    write_syslog "EXIT ${EXIT_CODE} - output logs"

    sudo pkill -f "${OTBR_AGENT}" || true
    sudo pkill -f "${OT_CTL}" || true
    sudo pkill -f "${OT_CLI}" || true
    sudo pkill -f "${OT_NCP}" || true
    wait

    echo 'clearing all'
    sudo rm /etc/dbus-1/system.d/otbr-agent.conf || true
    sudo rm -rf "${STAGE_DIR}" || true
    sudo rm -rf "${BUILD_DIR}" || true

    exit_message="Test teardown"
    echo "EXIT ${EXIT_CODE}: MESSAGE: ${exit_message}"
    exit ${EXIT_CODE}
}

restart_avahi_daemon()
{
    # Restart the avahi-daemon on the host to remove stale records from previous test runs.
    sudo service avahi-daemon restart
    sleep 1
}

otbr_exec_expect_script()
{
    local log_file="tmp/log_expect"

    for script in "$@"; do
        echo -e "\n${OTBR_COLOR_PASS}EXEC${OTBR_COLOR_NONE} ${script}"
        sudo killall ot-rcp || true
        sudo killall ot-cli || true
        sudo killall ot-cli-ftd || true
        sudo killall ot-cli-mtd || true
        sudo killall ot-ncp-ftd || true
        sudo killall ot-ncp-mtd || true
        sudo rm -rf tmp
        mkdir tmp
        # avahi-browser is used to discover the service and verify in the test cases no matter what OTBR_MDNS is.
        restart_avahi_daemon
        {
            sudo -E expect -df "${script}" 2>"${log_file}"
        } || {
            local EXIT_CODE=$?

            echo -e "\n${OTBR_COLOR_FAIL}FAIL${OTBR_COLOR_NONE} ${script}"
            cat "${log_file}" >&2
            return "${EXIT_CODE}"
        }
        echo -e "\n${OTBR_COLOR_PASS}PASS${OTBR_COLOR_NONE} ${script}"
        if [[ ${OTBR_VERBOSE} == 1 ]]; then
            cat "${log_file}" >&2
        fi
    done
}

do_expect()
{
    if [[ $# != 0 ]]; then
        otbr_exec_expect_script "$@"
    else
        mapfile -t test_files < <(find "${EXPECT_SCRIPT_DIR}" -type f -name "ncp_*.exp")
        otbr_exec_expect_script "${test_files[@]}" || die "ncp expect script failed!"
    fi

    exit 0
}

print_usage()
{
    cat <<EOF
USAGE: $0 COMMAND

COMMAND:
    build_ot_sim         Build simulated ot-cli-ftd and ot-ncp-ftd for testing.
    build_otbr_docker    Build otbr docker image for testing.
    expect               Run expect tests for otbr NCP mode.
    help                 Print this help.

EXAMPLES:
    $0 build_ot_sim build_otbr_docker expect
EOF
    exit 0
}

main()
{
    if [[ $# == 0 ]]; then
        print_usage
    fi

    export EXP_TUN_NAME="${TUN_NAME}"
    export EXP_LEADER_NODE_ID="${LEADER_NODE_ID}"
    export EXP_OTBR_DOCKER_IMAGE="${OTBR_DOCKER_IMAGE}"
    export EXP_OT_COMMISSIONER_PATH="${OT_COMMISSIONER_PATH}"

    while [[ $# != 0 ]]; do
        case "$1" in
            build_ot_sim)
                do_build_ot_simulation
                ;;
            build_otbr_docker)
                do_build_otbr_docker
                ;;
            build_ot_commissioner)
                do_build_ot_commissioner
                ;;
            expect)
                shift
                test_setup
                do_expect "$@"
                ;;
            help)
                print_usage
                ;;
            *)
                echo
                echo -e "${OTBR_COLOR_FAIL}Warning:${OTBR_COLOR_NONE} Ignoring: '$1'"
                ;;
        esac
        shift
    done
}

main "$@"
